<?php
/**
 * @link http://www.tintsoft.com/
 * @copyright Copyright (c) 2013 TintSoft LLC
 * @license http://www.tintsoft.com/license/
 */
namespace Leaps\Base;
use Closure;
use ArrayAccess;
use ReflectionParameter;
class Container implements ArrayAccess
{
    /**
     * 所有已经绑定到容器中的服务
     *
     * @var array
     */
    protected $bindings = array ();

    /**
     * 容器的共享实例
     *
     * @var array
     */
    protected $instances = array ();

    /**
     * 注册的别名服务
     *
     * @var array
    */
    protected $aliases = array ();

    protected $resolvingCallbacks = array ();

    public function bound($abstract)
    {
        return isset ( $this [$abstract] ) or isset ( $this->instances [$abstract] );
    }

    /**
     * 绑定服务到容器中
     * @param string $abstract
     * @param Closure|string|null $concrete
     * @param bool $shared
    */
    public function bind($abstract, $concrete = null, $shared = false)
    {
        if ( is_array ( $abstract ) ) {
            list ( $abstract, $alias ) = $this->extractAlias ( $abstract );
            $this->alias ( $abstract, $alias );
        }
        unset ( $this->instances [$abstract] );
        if ( is_null ( $concrete ) ) {
            $concrete = $abstract;
        }
        if ( ! $concrete instanceof Closure ) {
            $concrete = function ($c) use($abstract, $concrete)
            {
                $method = ($abstract == $concrete) ? 'build' : 'make';

                return $c->$method ( $concrete );
            };
        }
        $this->bindings [$abstract] = compact ( 'concrete', 'shared' );
    }

    public function bindIf($abstract, $concrete = null, $shared = false)
    {
        if ( ! $this->bound ( $abstract ) ) {
            $this->bind ( $abstract, $concrete, $shared );
        }
    }

    public function singleton($abstract, $concrete = null)
    {
        return $this->bind ( $abstract, $concrete, true );
    }

    public function share(Closure $closure)
    {
        return function ($container) use($closure)
        {
            static $object;
            if ( is_null ( $object ) ) {
                $object = $closure ( $container );
            }
            return $object;
        };
    }

    public function extend($abstract, Closure $closure)
    {
        if ( ! isset ( $this->bindings [$abstract] ) ) {
            throw new \InvalidArgumentException ( "Type {$abstract} is not bound." );
        }
        $resolver = $this->bindings [$abstract] ['concrete'];

        $this->bind ( $abstract, function ($container) use($resolver, $closure)
        {
            return $closure ( $resolver ( $container ), $container );
        }, $this->isShared ( $abstract ) );
    }

    public function instance($abstract, $instance)
    {
        if ( is_array ( $abstract ) ) {
            list ( $abstract, $alias ) = $this->extractAlias ( $abstract );

            $this->alias ( $abstract, $alias );
        }

        $this->instances [$abstract] = $instance;
    }

    public function alias($abstract, $alias)
    {
        $this->aliases [$alias] = $abstract;
    }

    public function make($abstract, $parameters = array())
    {
        $abstract = $this->getAlias ( $abstract );
        if ( isset ( $this->instances [$abstract] ) ) {
            return $this->instances [$abstract];
        }
        $concrete = $this->getConcrete ( $abstract );
        if ( $this->isBuildable ( $concrete, $abstract ) ) {
            $object = $this->build ( $concrete, $parameters );
        } else {
            $object = $this->make ( $concrete, $parameters );
        }
        if ( $this->isShared ( $abstract ) ) {
            $this->instances [$abstract] = $object;
        }
        $this->fireResolvingCallbacks ( $object );
        return $object;
    }

    public function build($concrete, $parameters = array())
    {
        if ( $concrete instanceof Closure ) {
            return $concrete ( $this, $parameters );
        }
        $reflector = new \ReflectionClass ( $concrete );
        if ( ! $reflector->isInstantiable () ) {
            $message = "Target [$concrete] is not instantiable.";
            throw new BindingResolutionException ( $message );
        }
        $constructor = $reflector->getConstructor ();
        if ( is_null ( $constructor ) ) {
            return new $concrete ();
        }
        $parameters = $constructor->getParameters ();
        $dependencies = $this->getDependencies ( $parameters );

        return $reflector->newInstanceArgs ( $dependencies );
    }

    /**
     * 获取所有已经绑定的容器
     *
     * @return array
     */
    public function getBindings()
    {
        return $this->bindings;
    }

    /**
     * (non-PHPdoc)
     *
     * @see ArrayAccess::offsetExists()
     */
    public function offsetExists($key)
    {
        return isset ( $this->bindings [$key] );
    }

    /**
     * (non-PHPdoc)
     *
     * @see ArrayAccess::offsetGet()
     */
    public function offsetGet($key)
    {
        return $this->make ( $key );
    }

    /**
     * (non-PHPdoc)
     *
     * @see ArrayAccess::offsetSet()
     */
    public function offsetSet($key, $value)
    {
        if ( ! $value instanceof Closure ) {
            $value = function () use($value)
            {
                return $value;
            };
        }
        $this->bind ( $key, $value );
    }

    /**
     * (non-PHPdoc)
     *
     * @see ArrayAccess::offsetUnset()
     */
    public function offsetUnset($key)
    {
        unset ( $this->bindings [$key] );
    }

    protected function fireResolvingCallbacks($object)
    {
        foreach ( $this->resolvingCallbacks as $callback ) {
            call_user_func ( $callback, $object );
        }
    }

    protected function getConcrete($abstract)
    {
        if ( ! isset ( $this->bindings [$abstract] ) ) {
            return $abstract;
        } else {
            return $this->bindings [$abstract] ['concrete'];
        }
    }

    protected function isBuildable($concrete, $abstract)
    {
        return $concrete === $abstract or $concrete instanceof Closure;
    }

    protected function isShared($abstract)
    {
        $set = isset ( $this->bindings [$abstract] ['shared'] );
        return $set and $this->bindings [$abstract] ['shared'] === true;
    }

    protected function extractAlias(array $definition)
    {
        return array (
                key ( $definition ),
                current ( $definition )
        );
    }

    protected function getDependencies($parameters)
    {
        $dependencies = array ();
        foreach ( $parameters as $parameter ) {
            $dependency = $parameter->getClass ();
            if ( is_null ( $dependency ) ) {
                $dependencies [] = $this->resolveNonClass ( $parameter );
            } else {
                $dependencies [] = $this->make ( $dependency->name );
            }
        }
        return ( array ) $dependencies;
    }

    protected function resolveNonClass(ReflectionParameter $parameter)
    {
        if ( $parameter->isDefaultValueAvailable () ) {
            return $parameter->getDefaultValue ();
        } else {
            $message = "Unresolvable dependency resolving [$parameter].";
            throw new BindingResolutionException ( $message );
        }
    }

    public function resolving(Closure $callback)
    {
        $this->resolvingCallbacks [] = $callback;
    }



    /**
     * 获取服务别名
     * @param unknown $abstract
     * @return Ambigous <unknown, multitype:>
     */
    protected function getAlias($abstract)
    {
        return isset ( $this->aliases [$abstract] ) ? $this->aliases [$abstract] : $abstract;
    }
}